using System;
using System.Collections;
using System.Data;

namespace ODX.Core
{
    /// <summary>
    /// The base class to create persistable objects.
    /// </summary>
    /// <remarks>
    /// <example>
    /// <code>
    /// <![CDATA[
    /// [Table]
    /// public class Person : Entity
    /// {
    ///     public abstract string Name { get; set; }
    ///     public abstract DateTime BirthDate { get; set; }
    /// }
    /// class Program
    /// {
    ///     static void Main()
    ///     {
    ///         Session s = new Sessoin(
    ///             new XmlDataProvider("persons.xml"),
    ///             Assembly.GetExecutingAssembly());
    /// 
    ///         Person p = session.Create<Person>();
    /// 
    ///         p.Name = "John";
    ///         p.BirthDate = DateTime.Now;
    /// 
    ///         session.Save();
    ///     }
    /// }
    /// ]]>
    /// </code>
    /// Now explore <c>persons.xml</c> and see your data saved.
    /// </example>
    /// </remarks>
    public class Entity : ICloneable
    {
        private RowList rows;
        private static string wrappingID = null;
        private static Session wrappingSession = null;
        private string id;


        ///<summary>
        /// Identifier of the entity.
        ///</summary>
        /// <remarks>
        /// ODX uses GUIDs to identify entities. But as far as GUID itself is not an SQL-92 type
        /// ODX converts it to the 22-char base64-encoded string: <code>Convert.ToBase64String(Guid.NewGuid().ToByteArray())</code> 
        /// </remarks>
        public string ID { get { return id; } }

        /// <summary>
        /// Session which is the entity belongs to.
        /// </summary>
        public Session Session { get { return Session.GetSession(rows.Any); } }


        internal DataRow[] Rows { get { return rows.All; } }

        /// <summary>
        /// Normally, you do not call this constructor but define an abstract subclass and perform automatic proxy build.
        /// </summary>
        public Entity()
        {
            if (wrappingID != null)
            {
                Wrap(wrappingID);
                return;
            }

            Hashtable dataRows = new Hashtable(StringComparer.InvariantCultureIgnoreCase);

            string[] tables = wrappingSession.Pm.GetTypeTables(GetType());

            if (tables.Length == 0)
                throw new OdxException("Table attribute not set!!!!!!!!");

            id = Session.CreateID();
            foreach (string tableName in tables)
            {
                DataTable dt = wrappingSession.Schema.Tables[tableName];
                DataRow dr = dt.NewRow();
                dr["ID"] = id;
                dt.Rows.Add(dr);
                dataRows[tableName] = dr;
            }

            rows = new RowList(dataRows);

            Type type = GetType();
            string typeTable = wrappingSession.Pm.GetTypeTable(type);
            string typeDef = Polymorpher.GetTypeDef(type);
            while (typeTable != null)
            {
                typeDef = Polymorpher.GetTypeDef(type) ?? typeDef;
                DataRow row = GetRow(typeTable);

                if (typeDef != null &&
                    row.Table.Columns.Contains(Session.TypeDefField) &&
                    row[Session.TypeDefField] is DBNull)
                {
                    row[Session.TypeDefField] = typeDef;
                }

                type = type.BaseType;
                typeTable = wrappingSession.Pm.GetTypeTable(type);
            }

            wrappingSession.RegisterEntity(this);

            OnCreated();
        }

        private void Wrap(string entityID)
        {
            string[] tables = wrappingSession.Pm.GetTypeTables(GetType());
            if (tables.Length == 0)
                throw new OdxException("Table attribute not set!!!!!!!!");

            id = entityID;

            Hashtable dataRows = new Hashtable(StringComparer.InvariantCultureIgnoreCase);
            foreach (string tableName in tables)
            {
                DataRow dr = wrappingSession.FindRecord(tableName, entityID);
                dataRows[tableName] = dr;
            }

            rows = new RowList(dataRows);

            wrappingSession.RegisterEntity(this);
        }

        internal static Entity CreateWrapper(Session session, Type type, string ID)
        {
            Entity e;
            if (ID != null && (e = session.FindEntity(ID)) != null)
                return e;

            lock (typeof(Entity))
            {
                try
                {
                    wrappingID = ID;
                    wrappingSession = session;
                    return (Entity)Activator.CreateInstance(type);
                }
                finally { wrappingID = null; wrappingSession = null; }
            }
        }

        internal static Entity CreateWrapper(DataRow row)
        {
            string ID = row["ID"].ToString();
            Session session = Session.GetSession(row);
            Entity e = session.FindEntity(ID);
            if (e != null)
                return e;

            Type type = session.Pm.DefineType(row);

            return CreateWrapper(session, type, ID);
        }

        internal void FixupReferences()
        {
            foreach (DataRow dr in new ArrayList(rows))
            {
                if (dr.RowState == DataRowState.Detached || dr.RowState == DataRowState.Deleted)
                {
                    DataRow validRow = dr.Table.Rows.Find(id);
                    if (validRow != null)
                        rows[dr.Table.TableName] = validRow;
                }
            }
        }

        internal DataRow GetRow(string tableName)
        {
            return rows[tableName];
        }

        protected Entity GetParent(string dataRelation)
        {
            return GetParent(Session.Schema.Relations[dataRelation]);
        }

        protected Entity GetParent(DataRelation dataRelation)
        {
            DataRow r = GetRow(dataRelation.ChildTable.TableName);
            object parentID = r[dataRelation.ChildColumns[0].ColumnName];
            if (parentID is DBNull)
                return null;
            DataRow parent = Session.FindRecord(dataRelation.ParentTable.TableName, parentID.ToString());
            if (parent != null)
                return CreateWrapper(parent);
            return null;
        }

        protected void SetParent(string dataRelation, Entity e)
        {
            SetParent(Session.Schema.Relations[dataRelation], e);
        }

        protected void SetParent(DataRelation dataRelation, Entity e)
        {
            DataRow r = GetRow(dataRelation.ChildTable.TableName);
            if (e == null)
                r[dataRelation.ChildColumns[0].ColumnName] = DBNull.Value;
            else
                r[dataRelation.ChildColumns[0].ColumnName] = e.ID;
        }

        /// <summary>
        /// Returns wheather the entity was not saved to the data source.
        /// </summary>
        public bool IsNew
        {
            get
            {
                foreach (DataRow dr in rows)
                    if (dr.RowState != DataRowState.Added)
                        return false;
                return true;
            }
        }
        /// <summary>
        /// Determines whether object has changet persistent properties.
        /// </summary>
        public bool IsChanged
        {
            get
            {
                foreach (DataRow dr in rows)
                    if (dr.RowState != DataRowState.Unchanged)
                        return true;
                return false;
            }
        }
        /// <summary>
        /// Determines whether object is marked to be deleted on Session.Save()
        /// </summary>
        public bool IsDeleted
        {
            get
            {
                foreach (DataRow dr in rows)
                    if (dr.RowState == DataRowState.Deleted || dr.RowState == DataRowState.Detached)
                        return true;
                return false;
            }
        }

        /// <summary>
        /// Marks object for deletion
        /// </summary>
        public void Delete()
        {
            foreach (DataRow dr in rows)
                dr.Delete();
        }

        protected internal void SetProperty(string tableName, string propertyName, object value)
        {
            GetRow(tableName)[propertyName] = value;
        }

        protected internal object GetProperty(string tableName, string propertyName, DataRowVersion version)
        {
            return GetRow(tableName)[propertyName, version];
        }

        protected internal object GetProperty(string tableName, string propertyName)
        {
            return GetProperty(tableName, propertyName, DataRowVersion.Default);
        }

        protected internal void SetProperty(string propertyName, object value)
        {
            bool found = false;
            foreach (DataRow row in rows)
            {
                if (row.Table.Columns.Contains(propertyName))
                {
                    row[propertyName] = value;
                    found = true;
                }
            }

            if (!found)
                throw new OdxException("Wrong field name specified!!!");
        }

        protected internal object GetProperty(string propertyName, DataRowVersion version)
        {
            foreach (DataRow row in rows)
                if (row.Table.Columns.Contains(propertyName))
                    return row[propertyName, version];

            throw new OdxException("Wrong field name specified!!!");
        }

        protected internal object GetProperty(string propertyName)
        {
            return GetProperty(propertyName, DataRowVersion.Default);
        }

        protected internal virtual void OnCreated() { }
        protected internal virtual void OnChanged(DataColumnChangeEventArgs e) { }
        protected internal virtual void OnChanging(DataColumnChangeEventArgs e) { }
        protected internal virtual void OnDeleted(DataRowChangeEventArgs e) { }
        protected internal virtual void OnDeleting(DataRowChangeEventArgs e) { }

        /// <summary>
        /// Undoes changes made after the last Session.Save()
        /// </summary>
        public void RejectChanges()
        {
            foreach (DataRow row in rows)
                row.RejectChanges();
        }

        internal void Refresh()
        {
            rows.Refresh();
        }


        object ICloneable.Clone()
        {
            return Clone();
        }
        /// <summary>
        /// Creates new object with new ID and copies other properties into new object.
        /// </summary>
        /// <returns></returns>
        public Entity Clone()
        {
            string newID = Session.CreateID();
            foreach (DataRow dr in rows)
            {
                DataRow newDR = dr.Table.NewRow();
                foreach (DataColumn dc in dr.Table.Columns)
                {
                    if (dr[dc] is ICloneable)
                        newDR[dc] = ((ICloneable)dr[dc]).Clone();
                    else
                        newDR[dc] = dr[dc];
                }

                newDR["ID"] = newID;
                dr.Table.Rows.Add(newDR);
            }
            return CreateWrapper(Session, GetType(), newID);
        }

        internal void Impersonate(Session tmp, string newID)
        {
            lock (typeof(Entity))
            {
                wrappingSession = tmp;
                Wrap(newID);
            }
        }
    }
}
